﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using ICSharpCode.SharpZipLib.Core;
using ICSharpCode.SharpZipLib.Zip;
using System.Xml.Linq;
using System.Xml;

namespace OpportunityCustomerIdFix.Controller
{
    public class CustomerIdFixController
    {
        public string InputFilePath { get; set; }
        public string TempFolderPath { get; set; }

        public CustomerIdFixController(string inputFilePath)
        {
            this.InputFilePath = inputFilePath;
            this.TempFolderPath = Path.Combine(Path.GetDirectoryName(InputFilePath), @"temp_files\");
        }

        // Unzips the specified file into a temporary folder in the same directory as the specified file
        public bool UnzipFile()
        {
            try
            {
                FileStream inStream = File.OpenRead(InputFilePath);
                ZipFile zf = new ZipFile(inStream);

                foreach (ZipEntry zipEntry in zf)
                {
                    String entryFileName = zipEntry.Name;

                    byte[] buffer = new byte[4096];     // 4K is optimum
                    Stream zipStream = zf.GetInputStream(zipEntry);

                    // Manipulate the output filename here as desired.
                    String fullZipToPath = Path.Combine(TempFolderPath, entryFileName);
                    string directoryName = Path.GetDirectoryName(fullZipToPath);
                    if (directoryName.Length > 0)
                    {
                        Directory.CreateDirectory(directoryName);
                    }

                    // Unzip file in buffered chunks. This is just as fast as unpacking to a buffer the full size
                    // of the file, but does not waste memory.
                    // The "using" will close the stream even if an exception occurs.
                    using (FileStream streamWriter = File.Create(fullZipToPath))
                    {
                        StreamUtils.Copy(zipStream, streamWriter, buffer);
                    }
                }

                zf.IsStreamOwner = true; // Makes close also shut the underlying stream
                zf.Close(); // Ensure we release resources

                return ProcessCustomizations();
            }
            // Ensures the temporary folder is always removed even if an error is encountered
            finally
            {
                Directory.Delete(TempFolderPath, true);
            }
        }

        // Navigates the Customizations.xml file and changes the "requiredlevel" attribute 
        // value from "systemrequired" to "required"
        // This change is made only on the "opportunity_customer_accounts" and "opportunity_customer_contacts" nodes
        // Only returns "true" if the Customizations.xml file has been updated
        public bool ProcessCustomizations()
        {
            bool updated = false;

            string customizationsPath = Path.Combine(TempFolderPath, @"customizations.xml");

            XmlDocument xmlFile = new XmlDocument();
            xmlFile.Load(customizationsPath);

            XmlNodeList nodes = xmlFile.SelectNodes("/ImportExportXml/EntityRelationships/EntityRelationship") as XmlNodeList;
            foreach(XmlNode node in nodes)
            {
                if (node.Attributes["Name"].Value.Equals("opportunity_customer_accounts") || node.Attributes["Name"].Value.Equals("opportunity_customer_contacts"))
                {
                    foreach(XmlNode child in node)
                    {
                        if (child.Name.Equals("field"))
                        {
                            if (child.Attributes["requiredlevel"].Value.Equals("systemrequired"))
                            {
                                updated = true;
                                child.Attributes["requiredlevel"].Value = "required";
                            }
                        }
                    }
                }
            }
            if (updated)
            {
                xmlFile.Save(customizationsPath);
                CreateZipFile();
            }

            return updated;
        }

        // Re-zips all extracted files from the initial zip file with the newly changed Customizations.xml file
        // New zip file given same name with "(fixed)" added on the end
        // If file with same name exists an incremental number such as "(1)" will be added
        public void CreateZipFile()
        {
            string zipFileName = Path.GetFileNameWithoutExtension(InputFilePath) + "_fixed.zip";
            string zipSave = Path.Combine(Path.GetDirectoryName(InputFilePath), zipFileName);
            
            int i = 1;
            while (File.Exists(zipSave))
            {
                zipFileName = String.Format("{0}_fixed_{1}.zip", Path.GetFileNameWithoutExtension(InputFilePath), i);
                zipSave = Path.Combine(Path.GetDirectoryName(InputFilePath), zipFileName);
                i++;
            }
            ZipOutputStream zOut = new ZipOutputStream(File.Create(zipSave));

            int folderOffset = TempFolderPath.Length + (TempFolderPath.EndsWith("\\") ? 0 : 1);

            CompressFolder(TempFolderPath, zOut, folderOffset);

            zOut.IsStreamOwner = true; // Makes the Close also Close the underlying stream
            zOut.Close();
        }

        // Compresses the folder - Allows for recursive calling in order to zip hierarchical folder structures
        public void CompressFolder(string path, ZipOutputStream zOut, int folderOffset)
        {
            byte[] buffer = new byte[4096];

            zOut.UseZip64 = UseZip64.Off;
            zOut.SetLevel(3);
            foreach (string file in Directory.GetFiles(path))
            {
                FileInfo fi = new FileInfo(file);

                string entryName = file.Substring(folderOffset); // Makes the name in zip based on the folder
                entryName = ZipEntry.CleanName(entryName); // Removes drive from name and fixes slash direction
                ZipEntry newEntry = new ZipEntry(entryName);
                newEntry.DateTime = fi.LastWriteTime; // Note the zip format stores 2 second granularity

                zOut.PutNextEntry(newEntry);
                using (FileStream fs = File.OpenRead(file))
                {
                    StreamUtils.Copy(fs, zOut, buffer);
                }
            }

            string[] folders = Directory.GetDirectories(path);
            foreach (string folder in folders)
            {
                CompressFolder(folder, zOut, folderOffset);
            }
        }
    }
}